package cn.ric.mycurd.plugin;

import java.util.Map;
import java.util.Properties;

import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.scripting.defaults.RawSqlSource;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.Assert;

/**
 * @author ric.zhang
 */
@Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
    @Signature(type = Executor.class, method = "query",
        args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
public class DynamicSqlInterceptor implements Interceptor {
    private static final Logger log = LoggerFactory.getLogger(DynamicSqlInterceptor.class);

    /**
     * 有些 sql 可能需要知道数据库类型才能生成
     */
    private BaseSqlBuilder baseSqlBuilder;
    private int debugTime = 0;
    private int infoTime = 100;
    private int warnTime = 1000;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        long currentTimeMillis = System.currentTimeMillis();
        final Object[] args = invocation.getArgs();
        MappedStatement ms = (MappedStatement) args[0];
        SqlSource sqlSource = ms.getSqlSource();
        final Object param = args[1];
        try {
            if (!(sqlSource instanceof RawSqlSource)) {
                return invocation.proceed();
            }
            String sql = buildSql(ms, sqlSource, param);
            if (sql != null) {
//                log.info("build sql, id={}, sql={}", ms.getId(), sql);
                // 重新设置 MappedStatement，使新 sql 生效
                SqlSource newSqlSource =
                    new RawSqlSource(ms.getConfiguration(), sql, param == null ? Map.class : param.getClass());
                args[0] = copyFromMappedStatement(ms, newSqlSource);
            }
            return invocation.proceed();
        } catch (Throwable t) {
            Throwable cause = t;
            while (cause.getCause() != null) {
                cause = cause.getCause();
            }
            log.warn("\n\t!!! fail to execute sql !!! \n   id: {}\n   error: {}", ms.getId(), cause.getMessage());
            throw t;
        } finally {
            long l = System.currentTimeMillis() - currentTimeMillis;
            if (l >= warnTime) {
                log.warn("{} ms used to execute this sql id: {}", l, ms.getId());
            } else if (l >= infoTime) {
                log.info("{} ms used to execute this sql id: {}", l, ms.getId());
            } else if (l >= debugTime) {
                log.debug("{} ms used to execute this sql id: {}", l, ms.getId());
            }
        }
    }

    /**
     * 创建 CURD 四类 SQL
     **/
    private String buildSql(MappedStatement ms, SqlSource sqlSource, Object param) {
        // 判断是否已经初始化，未初始化则放置默认参数
        if (baseSqlBuilder == null) {
            baseSqlBuilder = new MysqlBuilder();
            // Java字段名 - 数据库函数
            baseSqlBuilder.insertValues.put("modifyTime", "CURRENT_TIMESTAMP");
            baseSqlBuilder.insertValues.put("modify_time", "CURRENT_TIMESTAMP");
            baseSqlBuilder.insertValues.put("createTime", "CURRENT_TIMESTAMP");
            baseSqlBuilder.insertValues.put("create_time", "CURRENT_TIMESTAMP");

            baseSqlBuilder.updateValues.put("modifyTime", "CURRENT_TIMESTAMP");
            baseSqlBuilder.updateValues.put("modify_time", "CURRENT_TIMESTAMP");
        }
        String sql;
        final String mapperMethodName = ms.getId().replaceAll("^.*\\.(.*?)$", "$1");
        final String fieldmatcher = "\\w+?(And\\w+?)*";
        switch (sqlSource.getBoundSql(null).getSql()) {
            case DynamicSql.DYNAMIC_INSERT:
                Assert.notNull(param, "arg can't be null.");
                sql = baseSqlBuilder.buildCreateSql(param.getClass(), ms.getKeyProperties());
                break;
            case DynamicSql.DYNAMIC_UPDATE:
                final String updateAllRegex = String.format("^updateAllFieldsBy%s$", fieldmatcher);
                final String updateListedRegex = String.format("^updateListedFieldsBy%s$", fieldmatcher);
                final String updateFieldByFieldRegex = String.format("^update%sBy%s$", fieldmatcher, fieldmatcher);
                if (mapperMethodName.matches(updateAllRegex)) {
                    Assert.notNull(param, "arg can't be null.");
                    final String[] whereJavaFieldNames = mapperMethodName.substring(17).split("And");
                    Assert.notEmpty(whereJavaFieldNames, "key can't be empty.");
                    sql = baseSqlBuilder.buildUpdateAllFieldSql(param.getClass(), whereJavaFieldNames);
                } else if (mapperMethodName.matches(updateListedRegex)) {
                    final String[] keyFields = mapperMethodName.substring(20).split("And");
                    Assert.notEmpty(keyFields, "key can't be empty.");
                    // 此时需更新的字段以变长参数传入，即存在两个参数，第一个为 PO 实例，第二个为字段名数组
                    Map<String, ?> map = (Map<String, ?>) param;
                    Object bean = map.get("param1");
                    Assert.notNull(bean, "arg can't be null.");
                    String[] fieldNames = (String[]) map.get("param2");
                    Assert.notEmpty(fieldNames, "field names can't be empty.");
                    sql = baseSqlBuilder.buildUpdateListedFieldsSql(bean.getClass(), keyFields, fieldNames);
                } else if (mapperMethodName.matches(updateFieldByFieldRegex)) {
                    Assert.notNull(param, "arg can't be null.");
                    final String[] keyFields =
                        mapperMethodName.substring(mapperMethodName.indexOf("By") + 2).split("And");
                    Assert.notEmpty(keyFields, "key can't be empty.");
                    final String[] fieldNames =
                        mapperMethodName.substring(6, mapperMethodName.indexOf("By")).split("And");
                    sql = baseSqlBuilder.buildUpdateSomeFieldsByPo(param.getClass(), keyFields, fieldNames);
                } else {
                    sql = null;
                }
                break;
            case DynamicSql.DYNAMIC_SELECT:
                final String countAllRegex = String.format("^countBy%s$", fieldmatcher);
                final String listAllRegex = String.format("^listAllFieldsBy%sOrderBy%s$", fieldmatcher, fieldmatcher);
                final String listFieldByFieldRegex = String.format("^list%sBy%sOrderBy%s$", fieldmatcher, fieldmatcher, fieldmatcher);
                final String getAllRegex = String.format("^getAllFieldsBy%s$", fieldmatcher);
                final String getFieldByFieldRegex = String.format("^get%sBy%s$", fieldmatcher, fieldmatcher);
                Class<?> resultType = ms.getResultMaps().get(0).getType();
                if (mapperMethodName.matches(countAllRegex)) {
                    final String[] whereFields = mapperMethodName.substring(mapperMethodName.indexOf("By") + 2).split("And");
                    Assert.notEmpty(whereFields, "Query conditions can't be empty.");
                    sql = baseSqlBuilder.buildCountSql(resultType, whereFields);
                } else if (mapperMethodName.matches(listAllRegex)) {
                    final String[] whereFields = mapperMethodName.substring(mapperMethodName.indexOf("By") + 2, mapperMethodName.indexOf("OrderBy")).split("And");
                    Assert.notEmpty(whereFields, "Query conditions can't be empty.");
                    final String[] orderFields = mapperMethodName.substring(mapperMethodName.indexOf("OrderBy") + 7).split("And");
                    sql = baseSqlBuilder.buildListAllFieldSql(resultType, whereFields, orderFields);
                } else if (mapperMethodName.matches(listFieldByFieldRegex)) {
                    final String[] whereFields = mapperMethodName.substring(mapperMethodName.indexOf("By") + 2, mapperMethodName.indexOf("OrderBy")).split("And");
                    Assert.notEmpty(whereFields, "Query conditions can't be empty.");
                    final String[] updateFields = mapperMethodName
                            .substring(4, mapperMethodName.indexOf("By"))
                            .split("And");
                    final String[] orderFields = mapperMethodName.substring(mapperMethodName.indexOf("OrderBy") + 7).split("And");
                    sql = baseSqlBuilder.buildListSomeFieldsSql(resultType, whereFields, updateFields, orderFields);
                } else if (mapperMethodName.matches(getAllRegex)) {
                    final String[] whereFields = mapperMethodName.substring(mapperMethodName.indexOf("By") + 2).split("And");
                    Assert.notEmpty(whereFields, "Query conditions can't be empty.");
                    sql = baseSqlBuilder.buildSelectAllFieldsSql(resultType, whereFields);
                } else if (mapperMethodName.matches(getFieldByFieldRegex)) {
                    final String[] whereFields = mapperMethodName.substring(mapperMethodName.indexOf("By") + 2).split("And");
                    Assert.notEmpty(whereFields, "Query conditions can't be empty.");
                    final String[] updateFields = mapperMethodName
                        .substring(3, mapperMethodName.indexOf("By"))
                        .split("And");
                    sql = baseSqlBuilder.buildSelectSomeFieldsSql(resultType, whereFields, updateFields);
                } else {
                    sql = null;
                }
                break;
            case DynamicSql.DYNAMIC_DELETE:
                final String deleteRegex = String.format("^delete.*?By$", fieldmatcher);
                if (mapperMethodName.matches(deleteRegex)) {
                    Assert.notNull(param, "arg can't be null.");
                    final String[] keyFields = mapperMethodName.substring(8).split("And");
                    Assert.notEmpty(keyFields, "key can't be empty.");
                    sql = baseSqlBuilder.buildDeleteSql(param.getClass(), keyFields);
                } else {
                    sql = null;
                }
                break;
            default:
                sql = null;
                break;
        }
        return sql;
    }

    private MappedStatement copyFromMappedStatement(MappedStatement ms, SqlSource newSqlSource) {
        MappedStatement.Builder builder =
            new MappedStatement.Builder(ms.getConfiguration(), ms.getId(), newSqlSource, ms.getSqlCommandType());
        builder.resource(ms.getResource());
        builder.fetchSize(ms.getFetchSize());
        builder.statementType(ms.getStatementType());
        builder.keyGenerator(ms.getKeyGenerator());
        if (ms.getKeyProperties() != null && ms.getKeyProperties().length > 0) {
            builder.keyProperty(ms.getKeyProperties()[0]);
        }
        builder.timeout(ms.getTimeout());
        builder.parameterMap(ms.getParameterMap());
        builder.resultMaps(ms.getResultMaps());
        builder.resultSetType(ms.getResultSetType());
        builder.cache(ms.getCache());
        builder.flushCacheRequired(ms.isFlushCacheRequired());
        builder.useCache(ms.isUseCache());
        return builder.build();
    }

    @Override
    public Object plugin(Object target) {
        if (Executor.class.isAssignableFrom(target.getClass())) {
            return Plugin.wrap(target, this);
        } else {
            return target;
        }
    }

    @Override
    public void setProperties(Properties properties) {
        final String provider = properties.getProperty("provider", "mysql");
        baseSqlBuilder = BaseSqlBuilder.getInstance(provider);
        for (Map.Entry<Object, Object> entry : properties.entrySet()) {
            String key = entry.getKey().toString();
            if ("debug".equalsIgnoreCase(key)) {
                debugTime = Integer.parseInt(entry.getValue().toString());
                continue;
            } else if ("info".equalsIgnoreCase(key)) {
                infoTime = Integer.parseInt(entry.getValue().toString());
                continue;
            } else if ("warn".equalsIgnoreCase(key)) {
                warnTime = Integer.parseInt(entry.getValue().toString());
                continue;
            }
            String string = key.substring(7);
            if (key.startsWith("insert.")) {
                baseSqlBuilder.insertValues.put(string, entry.getValue().toString());
            } else if (key.startsWith("update.")) {
                baseSqlBuilder.updateValues.put(string, entry.getValue().toString());
            }
        }
    }
}
